;;
;; Copyright (c) 2010, Justin Grant <justin at imagine27 dot com>
;; All rights reserved.

;; Redistribution and use in source and binary forms, with or without modification
;; are permitted provided that the following conditions are met:

;; Redistributions of source code must retain the above copyright notice, this list
;; of conditions and the following disclaimer.
;; Redistributions in binary form must reproduce the above copyright notice, this
;; list of conditions and the following disclaimer in the documentation and/or
;; other materials provided with the distribution.
;; Neither the name of the <ORGANIZATION> nor the names of its contributors may be
;; used to endorse or promote products derived from this software without specific
;; prior written permission.
;;
;; THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
;; ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
;; WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
;; DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
;; ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
;; (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
;; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
;; THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
;; NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE
;; EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
;;

(set! *warn-on-reflection* true)

(ns clijx.math
  (:use [clijx.data]))


;; misc

(defn limit
  "returns a number based on a min and max limit"
  [min max val]
  (cond (> val max) max
        (< val min) min
        :else val))

(defn polygon-area
  "Calculates the signed area of a polygon given a sequence of points (in order).
   Assumes that the last point is connected to the first point.
   http://mathworld.wolfram.com/PolygonArea.html"
  [pts]
  (if (empty? pts)
    0
    (letfn [(matrix-det [pt1 pt2]
              ;;(println pt1 pt2)
              (- (* (first pt1) (second pt2))
                 (* (first pt2) (second pt1))))]
      (let [[iarea last-pt]
            (reduce (fn [res pt]
                      (vector (+ (first res)
                                 (matrix-det (second res) pt)) pt))
                    (vector 0 (first pts)) (rest pts))]
        (* 0.5 (+ iarea (matrix-det last-pt (first pts))))))))


;; points

(defn point-in-list
  "determines if a point exists in a sequence of points"
  [pts pt]
  (reduce (fn [res pte] (or res (= pte pt))) false pts))

(defn between
  "Given a num determines if it is between num1 and num2"
  [num1 num2 num]
  (let [[low high] (if (> num1 num2) [num2 num1] [num1 num2])]
    (and (<= low num) (>= high num))))

(defn point-on-line
  "Determines if a point exists on any lines defined by a sequence of points"
  [pts pt]
  (let [[pt-x pt-y] pt]
    (second (reduce
             (fn [res pte]
               (let [[[pte-x pte-y] [[last-pt-x last-pt-y] rbool]]
                     (vector pte res)]
                 (vector pte (or rbool
                                 (cond
                                  (= pt-x last-pt-x)
                                  (between pte-y last-pt-y pt-y)
                                  (= pt-y last-pt-y)
                                  (between pte-x last-pt-x pt-x))))))
             (vector (first pts) nil) (rest pts)))))

(defn add-point
  "Add a cartesian point to a sequence of points.
   Unchanged if point already exists in the sequence."
  [pts pt]
  (if (not pts)
    (vector pt)
    (if (point-in-list pts pt)
      pts
      (conj pts pt))))

(defmacro set-neighbor-on-axis
  "Set nearest point neighbor on axis if closer than existing neighbor"
  [res ptn ptnav ptav fun1 fun2 axis-fun]
  `(let [ptndelta# (- ~ptnav ~ptav)
         pt#  (if (< ptndelta# 0) (~res ~fun1) (~res ~fun2))]
     (if (and (not (= ptndelta# 0))
              (or (not pt#)
                  (< (Math/abs ^Float ptndelta#)
                     (Math/abs ^Float (- ~ptav (~axis-fun pt#))))))
       (if (< ptndelta# 0)
         (assoc ~res ~fun1 ~ptn)
         (assoc ~res ~fun2 ~ptn))
       ~res)))

(defn find-neighbors
  "finds the north, west, east and south point neighbors if they exist."
  [pts [ptx pty :as pt]]
  (reduce (fn [res [ptnx ptny :as ptn]]
            (cond
             ;; sets the :west(second) and :east(fourth) neighbors
             (= pty ptny) (set-neighbor-on-axis
                           res ptn ptnx ptx 1 3 first)
             ;; sets the :north(first) and :south(third) neighbors
             (= ptx ptnx) (set-neighbor-on-axis
                           res ptn ptny pty 2 0 second)
             :else res))
          [nil nil nil nil] pts))

(defn find-left-right-neighbors
  [pts pt dir]
  (let [[[lind rind] neighbors]
        (list (first (get (:directions @game) dir)) (find-neighbors pts pt))]
    (list (neighbors lind) (neighbors rind))))

(defn find-closing-points
  "Find the remaining points by 'turning' until the start point is reached."
  [ppts pts spt cpt dir turn]
  (if (= spt cpt)
    ppts
    (let [[turn1 turn2] turn
          lr-neighbors (find-left-right-neighbors pts cpt dir)
          dirs         (second (get (:directions @game) dir))
          nei1         (apply turn1 (list lr-neighbors))
          nei2         (apply turn2 (list lr-neighbors))
          next         (if nei1 nei1 nei2)
          ndir         (if nei1
                         (apply turn1 (list dirs))
                         (apply turn2 (list dirs)))]
      (recur (into ppts [next]) pts spt next ndir turn))))

(defn find-enclosing-polygon
  "Find the enclosing polygon given a 'trail' of points.
   The trail-pts must already have been added to all-pts otherwise we loop forever."
  [all-pts trail-pts dir]
  (let [spt        (first trail-pts)
        ept        (last trail-pts)
        left-turn  [first second]
        right-turn [second first]]
    (reduce (fn [res turn]
              (let [poly (into trail-pts
                               (rest (find-closing-points
                                      [ept] all-pts spt ept dir turn)))]
                ;; For now the smallest enclosing area is picked from the two.
                ;; Later when the stix is implemented we will pick the area
                ;; that it is not inside.
                (if (or (empty? res)
                        (< (Math/abs ^Float (polygon-area poly))
                           (Math/abs ^Float (polygon-area res))))
                  poly res)))
            [] [left-turn right-turn])))



;; (def pts  [[0 0] [0 10] [10 10] [10 0]])
;; (def tpts [[2 0] [2 4] [4 4] [4 0]])

;; (clijx.math/find-enclosing-polygon (into pts tpts) tpts :south)
;; ;; [[2 0] [2 4] [4 4] [4 0] [2 0]]

;; (clijx.math/find-closing-points [(last tpts)] (into pts tpts) (first tpts) (last tpts)
;;                                 :south [second first])
;; ;;[[4 0] [2 0]]

